Skip to content

feat(core): GlobeView pointer-anchored zoom (wheel + transitions)#10307

Closed
charlieforward9 wants to merge 9 commits into
visgl:masterfrom
NEW-HEAT:codex/globe-anchored-zoom
Closed

feat(core): GlobeView pointer-anchored zoom (wheel + transitions)#10307
charlieforward9 wants to merge 9 commits into
visgl:masterfrom
NEW-HEAT:codex/globe-anchored-zoom

Conversation

@charlieforward9

@charlieforward9 charlieforward9 commented May 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds opt-in pointer-anchored zoom for GlobeView while preserving the existing center-anchored default.

Latest stress pass: 5150bf4ee fixes a sporadic continuous-zoom fallback where zoomStart could miss the globe, leave startZoomLngLat empty, and then keep center-anchoring even after the pointer or touch centroid moved back over the globe. Pointer mode now recovers an anchor from the current zoom event and persists it for the rest of the gesture.

Screen.Recording.2026-06-06.at.3.53.35.PM.mov

Changes

  • Adds zoomAround?: 'center' | 'pointer' to controller options.
  • Uses GlobeViewport.panByGlobeAnchor for spherical pointer anchoring during wheel, pinch, and double-click zoom.
  • Adds shared globe ray/sphere helpers plus GlobeViewport.isPointOnGlobe and panByGlobeAnchor.
  • Adds a small near-limb grace band so pointer zoom does not fall back to center when the cursor barely misses the rendered sphere.
  • Falls back to planar panByPosition when GlobeView switches to WebMercatorViewport at high zoom.
  • Keeps center mode center-anchored by only applying transition around anchors when zoomAround: 'pointer' is active.
  • Keeps runtime controller option updates, such as deck.setProps({controller: {zoomAround}}), ahead of persisted controller state.
  • Recovers pointer anchors during continuous zoom when the initial zoom start did not produce a valid globe hit.
  • Keeps the beginner get-started globe example clean; manual regression controls live in the globe test app.

Validation

  • yarn vitest run --project headless test/modules/core/controllers/view-states.spec.ts
  • yarn vitest run --project headless test/modules/core/controllers/controllers.spec.ts
  • Earlier focused PR pass: yarn vitest run --project headless test/modules/core/viewports/globe-viewport.spec.ts test/modules/core/transitions/linear-interpolator.spec.ts test/modules/core/controllers/controllers.spec.ts
  • Targeted ESLint on modules/core/src/controllers/globe-controller.ts
  • Pre-commit hook: module/test/example ESLint + Prettier checks and node smoke tests passed with existing repo warnings only.
  • Manual local checks covered persisted Pointer mode, high-zoom fallback, constrained-pan pointer zoom, and the new missed-start recovery case.

Review

Requested another pass from @chrisgervang to validate the additional stress coverage and the continuous pointer-zoom recovery path.

Merge Notes

Default remains zoomAround: 'center', so existing GlobeView behavior is unchanged unless users opt in.

@coveralls

coveralls commented May 16, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 83.416% (+0.03%) from 83.391% — NEW-HEAT:codex/globe-anchored-zoom into visgl:master

@charlieforward9 charlieforward9 self-assigned this May 16, 2026
Replace the `log.warn('around not supported in GlobeView')` no-op
with real spherical anchoring, mirroring the existing planar branch:
- `initializeProps`: when the start viewport is a GlobeViewport and the
  screen anchor falls on the globe (`isPointOnGlobe`), unproject it to
  lng/lat and stash it as `aroundLngLat`.
- `interpolateProps`: each frame, call `panByGlobeAnchor(aroundLngLat,
  lerp(start.around, end.around, t))` so the geographic point stays
  pinned under the anchor screen point during the transition.

This makes the `_onDoubleClick` zoom transition (`_getTransitionProps
({around: pos})`) actually anchor on GlobeView. Previously the warn
fired and the LERP ran without anchor maintenance, which read as a
center-anchored zoom-in regardless of where the user tapped.

Tests cover the on-globe anchored path and the off-globe fall-through.
- Consolidate stray imports at the top of the file.
- Document the two GLOBE_ZOOM_ANCHOR_* constants so the empirical
  damping behavior (start damping at 0.75 of the limb, never below
  35% strength) is self-explanatory.
- Add a JSDoc to _getRayToGlobe explaining it as the shared ray/sphere
  math helper for unproject + isPointOnGlobe + panByGlobeAnchor.
@charlieforward9 charlieforward9 changed the title feat(core): add GlobeView pointer zoom option feat(core): GlobeView pointer-anchored zoom (wheel + transitions) May 22, 2026
Comment thread examples/get-started/pure-js/globe/app.js Outdated
Comment thread modules/core/src/viewports/globe-viewport.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 4ad99ed. Configure here.

Comment thread modules/core/src/controllers/globe-controller.ts Outdated
Comment thread modules/core/src/transitions/linear-interpolator.ts
* `rotateSpeedX` (number) - speed of rotation using shift + left/right arrow keys, in degrees. Default `15`.
* `rotateSpeedY` (number) - speed of rotation using shift + up/down arrow keys, in degrees. Default `10`.
* `dragMode` (string) - drag behavior without pressing function keys, one of `pan` and `rotate`.
* `zoomAround` (`'center' | 'pointer'`) - zoom anchor mode when supported by the controller. Default depends on the controller.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe zoomAround: pointer is default behavior for most controllers - not sure if this is necessary.

if (startPanCameraFrame !== undefined) s.startPanCameraFrame = startPanCameraFrame;
if (startPanAngularRate !== undefined) s.startPanAngularRate = startPanAngularRate;
if (startPanLockBearing !== undefined) s.startPanLockBearing = startPanLockBearing;
if (zoomAround !== undefined) s.zoomAround = zoomAround;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was investigating the intermittent issue where pointer-anchored zoom stops working after hot reload. I think this may help:

-    if (zoomAround !== undefined) s.zoomAround = zoomAround;
+    s.zoomAround = zoomAround || 'center';

The problem might be that _shouldZoomAroundPointer() reads from _state, but the conditional guard means if the constructor is ever called without zoomAround in options (which can happen during HMR when props haven’t fully propagated yet), the key is just absent from state and the feature silently disengages.

To reproduce: set controller: {zoomAround: 'pointer'}, confirm it works, then trigger a hot reload.. it should intermittently reverts to center-zoom.

Unlike the other fields guarded by !== undefined (which are transient gesture anchors that are legitimately absent between interactions), zoomAround is a config option that always needs a value. Can you test it?

@charlieforward9

Copy link
Copy Markdown
Collaborator Author

Superseded by the upstream-branch PR (moved off the fork per repo policy).

charlieforward9 added a commit that referenced this pull request Jun 16, 2026
…pout)

Per #10307 review: zoomAround is a config option that always needs a value,
unlike the transient gesture anchors guarded the same way. The !== undefined
guard let a partial-props / HMR reconstruction drop the key, so
_shouldZoomAroundPointer() saw undefined and silently reverted to center zoom.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants